use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not, path::{self, PathBuf, PrefixComponent}};

use anyhow::Result;

use crate::{path::{PathBufDyn, PathCow}, scheme::SchemeRef, url::{Encode, Url, UrlBuf, UrlCow}};

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Component<'a> {
	Scheme(SchemeRef<'a>),
	Prefix(PrefixComponent<'a>),
	RootDir,
	CurDir,
	ParentDir,
	Normal(&'a OsStr),
}

impl<'a> From<path::Component<'a>> for Component<'a> {
	fn from(comp: path::Component<'a>) -> Self {
		match comp {
			path::Component::Prefix(p) => Component::Prefix(p),
			path::Component::RootDir => Component::RootDir,
			path::Component::CurDir => Component::CurDir,
			path::Component::ParentDir => Component::ParentDir,
			path::Component::Normal(s) => Component::Normal(s),
		}
	}
}

impl<'a> FromIterator<Component<'a>> for Result<UrlBuf> {
	fn from_iter<I: IntoIterator<Item = Component<'a>>>(iter: I) -> Self {
		let mut buf = PathBuf::new();
		let mut scheme = None;
		iter.into_iter().for_each(|c| match c {
			Component::Scheme(s) => scheme = Some(s),
			Component::Prefix(p) => buf.push(path::Component::Prefix(p)),
			Component::RootDir => buf.push(path::Component::RootDir),
			Component::CurDir => buf.push(path::Component::CurDir),
			Component::ParentDir => buf.push(path::Component::ParentDir),
			Component::Normal(s) => buf.push(path::Component::Normal(s)),
		});

		Ok(if let Some(s) = scheme {
			UrlCow::try_from((s, PathCow::Owned(PathBufDyn::Os(buf))))?.into_owned()
		} else {
			buf.into()
		})
	}
}

impl<'a> FromIterator<Component<'a>> for PathBuf {
	fn from_iter<I: IntoIterator<Item = Component<'a>>>(iter: I) -> Self {
		let mut buf = Self::new();
		iter.into_iter().for_each(|c| match c {
			Component::Scheme(_) => {}
			Component::Prefix(p) => buf.push(path::Component::Prefix(p)),
			Component::RootDir => buf.push(path::Component::RootDir),
			Component::CurDir => buf.push(path::Component::CurDir),
			Component::ParentDir => buf.push(path::Component::ParentDir),
			Component::Normal(s) => buf.push(path::Component::Normal(s)),
		});
		buf
	}
}

// --- Components
#[derive(Clone)]
pub struct Components<'a> {
	inner:          path::Components<'a>,
	url:            Url<'a>,
	scheme_yielded: bool,
}

impl<'a> From<Url<'a>> for Components<'a> {
	fn from(value: Url<'a>) -> Self {
		Self { inner: value.loc().components(), url: value, scheme_yielded: false }
	}
}

impl<'a> Components<'a> {
	pub fn os_str(&self) -> Cow<'a, OsStr> {
		let path = self.inner.as_path();
		if self.url.kind().is_local() || self.scheme_yielded {
			return path.as_os_str().into();
		}

		let mut s = OsString::from(Encode(self.url).to_string());
		s.reserve_exact(path.as_os_str().len());
		s.push(path);
		s.into()
	}

	pub fn covariant(&self, other: &Self) -> bool {
		match (self.scheme_yielded, other.scheme_yielded) {
			(false, false) => {}
			(true, true) if self.url.scheme().covariant(other.url.scheme()) => {}
			_ => return false,
		}
		self.inner == other.inner
	}
}

impl<'a> Iterator for Components<'a> {
	type Item = Component<'a>;

	fn next(&mut self) -> Option<Self::Item> {
		if !self.scheme_yielded {
			self.scheme_yielded = true;
			Some(Component::Scheme(self.url.scheme()))
		} else {
			self.inner.next().map(Into::into)
		}
	}

	fn size_hint(&self) -> (usize, Option<usize>) {
		let (min, max) = self.inner.size_hint();
		let scheme = self.scheme_yielded.not() as usize;

		(min + scheme, max.map(|n| n + scheme))
	}
}

impl<'a> DoubleEndedIterator for Components<'a> {
	fn next_back(&mut self) -> Option<Self::Item> {
		if let Some(comp) = self.inner.next_back() {
			Some(comp.into())
		} else if !self.scheme_yielded {
			self.scheme_yielded = true;
			Some(Component::Scheme(self.url.scheme()))
		} else {
			None
		}
	}
}

impl<'a> FusedIterator for Components<'a> {}

impl<'a> PartialEq for Components<'a> {
	fn eq(&self, other: &Self) -> bool {
		Some(self.url.scheme()).filter(|_| !self.scheme_yielded)
			== Some(other.url.scheme()).filter(|_| !other.scheme_yielded)
			&& self.inner == other.inner
	}
}

// --- Tests
#[cfg(test)]
mod tests {
	use std::path::Path;

	use anyhow::Result;

	use super::*;
	use crate::url::UrlLike;

	#[test]
	fn test_collect() -> Result<()> {
		crate::init_tests();

		let search: UrlBuf = "search://keyword//root/projects/yazi".parse()?;
		assert_eq!(search.uri(), "");
		assert_eq!(search.scheme(), SchemeRef::Search { domain: "keyword", uri: 0, urn: 0 });

		let item = search.try_join("main.rs")?;
		assert_eq!(item.uri(), "main.rs");
		assert_eq!(item.scheme(), SchemeRef::Search { domain: "keyword", uri: 1, urn: 1 });

		let u: UrlBuf = item.components().take(4).collect::<Result<_>>()?;
		assert_eq!(u.scheme(), SchemeRef::Search { domain: "keyword", uri: 0, urn: 0 });
		assert_eq!(u.loc(), Path::new("/root/projects"));

		let u: UrlBuf = item
			.components()
			.take(5)
			.chain([Component::Normal(OsStr::new("target/release/yazi"))])
			.collect::<Result<_>>()?;
		assert_eq!(u.scheme(), SchemeRef::Search { domain: "keyword", uri: 0, urn: 0 });
		assert_eq!(u.loc(), Path::new("/root/projects/yazi/target/release/yazi"));

		Ok(())
	}
}
